A better Vimdiff Git mergetool
Git ships with support to invoke Vimdiff as a "mergetool" to help resolve merge conflicts. Unfortunately Vim struggles a bit with three-way diffs, both with highlighting the differences and with shuffling individual changes between the three windows. This article details a simpler way to use Vimdiff as a Git mergetool. Update: this is now a Vim plugin: https://github.com/whiteinge/diffconflicts Overview You are likely to encounter a merge conflict in Git eventually by merging one feature branch into another or by merging upstream changes into a local branch. Git is quite good at automatically resolving conflicts. A simplistic explanation is that Git starts by finding the common ancestor of your local changes and the changes you are merging into yours. Git then replays the history of each lineage, making intelligent choices along the way about how and where code is added or moved or renamed. If there are conflicts that Git cannot resolve itself the file is left in a conflicted state that must be resolved manually -- more on this below as the MERGED version of the file. The simplest way to resolve a Git merge conflict is to open the conflicted file with your favorite text editor and to manually find and remove the conflict markers, keeping the best version of the code from each side of the conflict, then marking the conflict as resolved in Git. Using Vimdiff as a mergetool Resolving manually is fine for small conflicts and for obvious conflicts, but this can be frustrating and error-prone for large conflicts and for conflicts where each side of the changes differ only subtly. Mergetools can help make short work of even gnarly merge conflicts. Invoke Vimdiff as a mergetool with git mergetool -t gvimdiff. Recent versions of Git invoke Vimdiff with the following window layout: +--------------------------------+ | LOCAL | BASE | REMOTE | +--------------------------------+ | MERGED | +--------------------------------+ ; LOCAL : A temporary file containing the contents of the file on the current branch. ; BASE : A temporary file containing the common base for the merge. ; REMOTE : A temporary file containing the contents of the file to be merged. ; MERGED : The file containing the conflict markers. Git has performed as much automatic conflict resolution as possible and the state of this file is a combination of both LOCAL and REMOTE with conflict markers surrounding anything that Git could not resolve itself. The mergetool should write the result of the resolution to this file. Vimdiff for three-way merges The default Vimdiff window layout presents quite a lot of useful information. In many cases, however, it is simply too much noise and hides the relevant visual clues you need to solve a conflict quickly. For example, here is the default view of the conflict shown above: Vimdiff is an excellent two-way diff viewer and many of the helper shortcuts are geared for moving changes between only two windows at a time. Displaying differences between three (or more!) versions of a file is a hard problem to solve and syntax highlighting alone just isn't well-suited to the job. Playing to Vimdiff strengths It is useful to be able to quickly see the remote version of the file, or the common ancestor of both files, or even your version of the file before any automatic merging was attempted. However, it is often most useful to view the simple comparison of just the "left" conflict and the "right" conflict. For example, here is the same conflict as above but only diffing the conflicts themselves: The resolution is much clearer and since this is two-way merge all of the Vimdiff keyboard shortcuts work as intended. An alternate Vimdiff mergetool In order to achieve the layout detailed above we need to step outside of the Vimdiff mergetool that ships with Git, and even outside of Git itself. The "diffconflicts" script in the section below can be used as a mergetool that will then invoke Vimdiff. Save the script below as "diffconflicts" somewhere on your shell PATH and mark it as executable (chmod +x diffconflicts). Next register it with Git as a custom mergetool by running the following commands (substitude "gvim" for "vim" if you prefer Gvim): git config --global merge.tool diffconflicts git config --global mergetool.diffconflicts.cmd 'diffconflicts vim $BASE $LOCAL $REMOTE $MERGED' git config --global mergetool.diffconflicts.trustExitCode true git config --global mergetool.keepBackup false This script invokes Vimdiff with three tabpages. The first tabpage is a two-way diff of each "side" of a conflict. This allows the full use of vanilla Vimdiff. Resolve conflicts in the left window; this is the file that will eventually be written to disk as the resolved file. If you wish to abort Vimdiff and signal to Git that the resolution is not complete, exit Vimdiff with :cq. This tells Vim to exit with an error code which then tells Git that the conflict was not resolved. # +------+ # | Tab1 | # +--------------------------------+ # | LCONFL | RCONFL | # +--------------------------------+ The second tabpage is the standard three-way diff of the pre-automerge "yours", "theirs", and the merge base. Because syntax highlighting in a three-way diff is so noisy, I have found mappings to quickly disable diff syntax highlighting in one window at a time to be very useful in this window layout. (Example mappings are included at the bottom.) # +------+ # | Tab2 | # +--------------------------------+ # | LOCAL | BASE | REMOTE | # +--------------------------------+ Finally, the third tabpage is the actual conflicted file that contains the conflict markers. This view shouldn't be necessary but I find it is helpful to quickly sanity-check the diff in the first tabpage in case the diffconflicts script did not work correctly. # +------+ # | Tab3 | # +--------------------------------+ # | <<<<<<< HEAD | # | LCONFL | # | | # | RCONFL | # | >>>>>>> someref | # +--------------------------------+ The diffconflicts script Note: the latest version of this script is on GitHub. #!/bin/bash # A better vimdiff mergetool for Git # # Git does a great job resolving merge conflicts automatically but there are # times when human intervention is still needed. Git resolves all the conflicts # that it is able to and finally wraps each conflict it cannot resolve within # special markers which must be resolved by a human. # # The vertical format and lack of syntax highlighting in the plain conflict # marker layout makes it difficult to spot subtle conflicts such as # single-character changes and this is where a two-way diff really shines! # To see this explained using screenshots, see: # http://vim.wikia.com/wiki/A_better_Vimdiff_Git_mergetool # # This script, when used as a Git mergetool, opens each "side" of the conflict # markers in a two-way vimdiff window. This combines all the awesome of Git's # automatic merging savvy with the awesome and simplicity of a simple two-way # diff. # # Add this mergetool to your ~/.gitconfig (you can substitute gvim for vim): # # git config --global merge.tool diffconflicts # git config --global mergetool.diffconflicts.cmd 'diffconflicts vim $BASE $LOCAL $REMOTE $MERGED' # git config --global mergetool.diffconflicts.trustExitCode true # git config --global mergetool.diffconflicts.keepBackup false # # The next time you perform a merge with conflicts, invoke this tool with the # following command. (Of course you can set it as your default mergetool as # well.) # # git mergetool --tool diffconflicts # # This tool can open three tabs in Vim that each provide a different way to # view the conflicts. You can resolve the conflicts in the first tab and save # and exit the file. This will also mark the conflict as resolved in Git. # Only the first tab is opened by default so Vim loads more quickly and also # because the other tabs are only occasionally useful for tough merges. To open # Tab2 and Tab3 use the mapping D. # # Tab1 is a two-way diff of just the conflicts. Resolve the conflicts here # and save the file. # +--------------------------------+ # | LCONFL | RCONFL | # +--------------------------------+ # Tab2 is a three-way diff of the original files and the merge base. This is # the traditional three-way diff. Although noisy, it is occasionally useful # to view the three original states of the conflicting file before the merge. # +--------------------------------+ # | LOCAL | BASE | REMOTE | # +--------------------------------+ # Tab3 is the in-progress merge that Git has written to the filesystem # containing the conflict markers. I.e., the file you would normally edit by # hand when not using a mergetool. # +--------------------------------+ # | <<<<<<< HEAD | # | LCONFL | # | | # | RCONFL | # | >>>>>>> someref | # +--------------------------------+ # # Workflow: # # 1. Save your changes to the LCONFL temporary file (the left window on the # first tab; also the only file that isn't read-only). # 2. The LOCAL, BASE, and REMOTE versions of the file are available in the # second tabpage if you want to look at them. # 3. When vimdiff exits cleanly, the file containing the conflict markers # will be updated with the contents of your LCONFL file edits. # # NOTE: Use :cq to abort the merge and exit Vim with an error code. if | $# != "5" ; then echo -e "Usage: $0 \$EDITOR \$BASE \$LOCAL \$REMOTE \$MERGED" exit 1 fi cmd=$1 BASE="$2" LOCAL="$3" REMOTE="$4" MERGED="$5" printf -v QBASE '%q' "${BASE}" printf -v QLOCAL '%q' "${LOCAL}" printf -v QREMOTE '%q' "${REMOTE}" printf -v QMERGED '%q' "${MERGED}" # Use gnu sed if on OSX for better portability (brew install gnu-sed) GNU_SED="gsed" type $GNU_SED >/dev/null 2>&1 || GNU_SED="sed" # Temporary files for left and right side, keeping file extensions for syntax highlighting MERGED_PATH="$(dirname ${MERGED})" MERGED_FILE="$(basename ${MERGED})" LCONFL="${MERGED_PATH}/.REMOTE.$$.${MERGED_FILE}" RCONFL="${MERGED_PATH}/.LOCAL.$$.${MERGED_FILE}" # Always delete our temp files; Git will handle it's own temp files trap 'rm -f "'"${LCONFL}"'" "'"${RCONFL}"'"' SIGINT SIGTERM EXIT # Remove the conflict markers for each 'side' and put each into a temp file $GNU_SED -r -e '/^<<<<<<< /,/^ \r?$/d' -e '/^>>>>>>> /d' "${MERGED}" > "${LCONFL}" $GNU_SED -r -e '/^ \r?$/,/^>>>>>>> /d' -e '/^<<<<<<< /d' "${MERGED}" > "${RCONFL}" # Fire up vimdiff $cmd -f -R -d "${LCONFL}" "${RCONFL}" \ -c ":set noro" \ -c ":nmap D :tabe $QLOCAL:vert diffs $QBASE:vert diffs $QREMOTE:winc t:tabe $QMERGED:tabfir" EC=$? # Overwrite $MERGED only if vimdiff exits cleanly. if $EC "0" ; then cat "${LCONFL}" > "${MERGED}" fi exit $EC Toggle diff highlighting in a single window " Disable one diff window during a three-way diff allowing you to cut out the " noise of a three-way diff and focus on just the changes between two versions " at a time. Inspired by Steve Losh's Splice function! DiffToggle(window) " Save the cursor position and turn on diff for all windows let l:save_cursor = getpos('.') windo :diffthis " Turn off diff for the specified window (but keep scrollbind) and move " the cursor to the left-most diff window exe a:window . "wincmd w" diffoff set scrollbind set cursorbind exe a:window . "wincmd " . (a:window 1 ? "l" : "h") " Update the diff and restore the cursor position diffupdate call setpos('.', l:save_cursor) endfunction " Toggle diff view on the left, center, or right windows nmap dl :call DiffToggle(1) nmap dc :call DiffToggle(2) nmap dr :call DiffToggle(3) References *This functionality as a Vim plugin: https://github.com/whiteinge/diffconflicts *The default vimdiff mergetool script that ships with Git: https://github.com/git/git/blob/master/mergetools/vimdiff *The diffconflicts script on GitHub (buried in my dotfiles repo): https://github.com/whiteinge/dotfiles/blob/master/bin/diffconflicts Related plugins The similar plugins below, as well as the excellent Fugitive, do not make use of Git's merge results and either perform a three-way diff of the two parents and the conflicted file, or they perform a two-way diff of the parents and ignore Git's merge results in the conflicted file. The whole point of the above explanation is to use as much of Git's attempted merge as possible. *Splice by Steve Losh * ? Not sure if this actually does 3-way diff. *Conflicted Similar workflow as described here but the two-way diff view ignores Git's automatic merge result. Comments This doesn't seem to work. After adding the script as my mergetool and attempting to run mergetool to resolve a conflict, I get: git config option merge.tool set to unknown tool: diffconflicts Resetting to default... merge tool candidates: opendiff kdiff3 tkdiff xxdiff meld tortoisemerge gvimdiff diffuse ecmerge p4merge araxis bc3 emerge vimdiff Any idea? --August 28, 2012 :I do not know anything about this tip, but a quick search for "diffconflicts" in the tip shows you need to create the script, put it in a directory in your PATH, and use chmod (as well as use the git config commands shown). If you get it working, please report here. JohnBeckett (talk) 04:39, August 29, 2012 (UTC) :SOLUTION: you need to follow the hints in the script (configure mergetool.diffconflicts). 10:55, October 27, 2012 (UTC) Category:VimDiff